<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>机器学习概念交互演示</title>
  <style>
    :root {
      --bg: #0b1324;
      --panel: #111a32;
      --muted: #7d8db5;
      --text: #e8eeff;
      --brand: #5ad6ff;
      --ok: #46d082;
      --warn: #ffb84d;
      --bad: #ff6b6b;
      --accent: #c0a6ff;
      --line: #1d2a52;
    }

    * {
      box-sizing: border-box
    }

    html,
    body {
      height: 100%
    }

    body {
      margin: 0;
      font: 16px/1.55 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
      color: var(--text);
      background: linear-gradient(160deg, #0b1324, #0c1a3a 45%, #0b1324);
      padding: 20px;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
      background: linear-gradient(180deg, #0e1730, #0a1226);
      border: 1px solid var(--line);
      border-radius: 16px;
      box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
      overflow: hidden;
    }

    .header {
      position: sticky;
      top: 0;
      z-index: 5;
      background: rgba(10, 18, 40, .75);
      backdrop-filter: blur(6px);
      border-bottom: 1px solid var(--line);
      text-align: center;
      padding: 22px 16px;
    }

    .header h1 {
      margin: 0 0 6px;
      font-size: 26px
    }

    .header p {
      margin: 0;
      color: var(--muted)
    }

    .content {
      padding: 28px
    }

    .section {
      margin-bottom: 28px;
      background: radial-gradient(120% 120% at 0% 0%, #0f2352, #0b1736 55%, #0a1531);
      border: 1px solid var(--line);
      border-left: 4px solid var(--brand);
      border-radius: 14px;
      padding: 20px;
      box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
    }

    .section h2 {
      margin: 0 0 14px;
      font-size: 20px
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 14px;
      align-items: center;
      margin: 10px 0 20px
    }

    .control-group {
      display: flex;
      flex-direction: column;
      gap: 6px;
      min-width: 180px
    }

    .control-group label {
      font-weight: 600;
      color: #c9d7ff;
      font-size: 14px
    }

    input[type="number"] {
      width: 110px;
      padding: 10px 12px;
      border-radius: 10px;
      background: #0f1e3e;
      color: var(--text);
      border: 1px solid #2d3e74;
      outline: none;
    }

    input[type="range"] {
      width: 180px;
      height: 6px;
      border-radius: 999px;
      outline: none;
      -webkit-appearance: none;
      background: #1f2a4d;
      border: 1px solid #2d3e74;
    }

    input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      background: var(--brand);
      box-shadow: 0 0 0 2px rgba(90, 214, 255, .15);
      cursor: pointer
    }

    input[type="range"]::-moz-range-thumb {
      width: 18px;
      height: 18px;
      border-radius: 50%;
      background: var(--brand);
      border: none;
      cursor: pointer
    }

    .legend {
      display: flex;
      gap: 18px;
      flex-wrap: wrap;
      margin: 8px 0 14px
    }

    .legend-item {
      display: flex;
      align-items: center;
      gap: 8px;
      color: #bcd0ff
    }

    .legend-color {
      width: 18px;
      height: 18px;
      border-radius: 50%
    }

    .data-visualization {
      margin-bottom: 18px
    }

    .dataset {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-content: flex-start;
      min-height: 120px;
      padding: 16px;
      border: 1px dashed #2c417b;
      border-radius: 12px;
      background: #0f1e3e;
    }

    .data-point {
      width: 30px;
      height: 30px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      font-weight: 700;
      font-size: .8em;
      cursor: pointer;
      transition: transform .2s, box-shadow .2s
    }

    .data-point:hover {
      transform: scale(1.08);
      box-shadow: 0 6px 16px rgba(0, 0, 0, .35)
    }

    .train {
      background: var(--ok)
    }

    .validation {
      background: var(--warn);
      color: #333
    }

    .test {
      background: var(--bad)
    }

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 14px;
      margin-top: 10px
    }

    .stat-card {
      background: #0f1e3e;
      border: 1px solid #2d3e74;
      border-radius: 10px;
      padding: 16px;
      text-align: center
    }

    .stat-number {
      font-size: 28px;
      font-weight: 800;
      color: var(--brand)
    }

    .stat-label {
      font-size: 14px;
      color: #b7c6ff
    }

    .training-demo {
      background: #0f1e3e;
      border: 1px solid #2d3e74;
      border-radius: 12px;
      padding: 16px;
      margin-top: 12px
    }

    .epoch-counter {
      text-align: center;
      font-size: 18px;
      font-weight: 700;
      color: var(--brand);
      margin: 6px 0 12px
    }

    .progress-bar {
      width: 100%;
      height: 18px;
      background: #1f2a4d;
      border: 1px solid #2d3e74;
      border-radius: 999px;
      overflow: hidden;
    }

    .progress-fill {
      height: 100%;
      background: linear-gradient(90deg, var(--brand), var(--accent));
      width: 0%;
      transition: width .3s ease
    }

    .batch-visualization {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
      gap: 10px;
      margin: 16px 0
    }

    .batch {
      border: 1px solid #2d3e74;
      border-radius: 10px;
      padding: 10px;
      text-align: center;
      background: #0e1d3f
    }

    .batch.active {
      border-color: var(--brand);
      box-shadow: 0 0 0 2px rgba(90, 214, 255, .15) inset;
      background: #11214a
    }

    .batch-title {
      font-size: 12px;
      font-weight: 700;
      color: #cbd7ff;
      margin-bottom: 6px
    }

    .mini-data {
      display: flex;
      flex-wrap: wrap;
      gap: 2px;
      justify-content: center
    }

    .mini-point {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--ok)
    }

    .btn {
      appearance: none;
      border: 1px solid #355099;
      background: #11214a;
      color: #eaf2ff;
      padding: .55rem .9rem;
      border-radius: 10px;
      cursor: pointer;
      font-weight: 700;
      transition: border-color .2s, transform .15s
    }

    .btn:hover {
      border-color: var(--brand)
    }

    .btn:active {
      transform: translateY(1px)
    }

    .info-box {
      background: #0e1d3f;
      border: 1px solid #2a3e79;
      border-radius: 10px;
      padding: 12px;
      color: #cfe0ff;
      margin: 10px 0
    }

    /* 知识测试样式 */
    .quiz-container {
      margin-top: 10px;
    }

    .quiz-question {
      font-size: 18px;
      margin-bottom: 12px;
      font-weight: 800;
      color: #e8eeff
    }

    .quiz-options {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-bottom: 12px;
    }

    .quiz-option {
      padding: 12px;
      background: #0f1e3e;
      border: 1px solid #2d3e74;
      border-radius: 8px;
      cursor: pointer;
      transition: all .2s;
    }

    .quiz-option:hover {
      border-color: var(--brand);
      background: #132553;
    }

    .quiz-option.correct {
      background: rgba(70, 208, 130, .2);
      border-color: rgba(70, 208, 130, .8);
    }

    .quiz-option.wrong {
      background: rgba(255, 107, 107, .2);
      border-color: rgba(255, 107, 107, .8);
    }

    .hint {
      margin-left: 10px;
      font-weight: 700
    }

    @media (max-width:720px) {
      .control-group {
        min-width: 130px
      }
    }

    /* 选项卡（Tabs）样式 */
    .tabs {
      display: flex;
      gap: 8px;
      margin: 0 0 12px;
      border-bottom: 1px solid var(--line)
    }

    .tab {
      appearance: none;
      border: 1px solid #2d3e74;
      border-bottom: none;
      background: #0e1d3f;
      color: #cfe0ff;
      padding: 10px 14px;
      border-radius: 10px 10px 0 0;
      cursor: pointer;
      font-weight: 700
    }

    .tab:hover {
      border-color: var(--brand)
    }

    .tab.active {
      background: #11214a;
      color: #fff;
      border-color: var(--brand)
    }

    .tab-panel {
      display: none
    }

    .tab-panel.active {
      display: block
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="header">
      <h1>🧠 机器学习概念交互演示</h1>
      <p>黑色主题 · 通过动手实践理解数据划分、Epoch、Iteration 和 Batch Size</p>
    </div>

    <div class="content">
      <div class="tabs">
        <button class="tab active" data-tab-target="part1">📊 数据集划分</button>
        <button class="tab" data-tab-target="part2">🎯 训练参数</button>
        <button class="tab" data-tab-target="part3">🧪 知识测试</button>
      </div>
      <!-- 数据划分部分 -->
      <div class="section tab-panel active" data-tab="part1">
        <h2>📊 第一部分：数据集划分</h2>
        <div class="info-box">
          <strong>概念速览：</strong>
          <br>• <strong>训练集 (Training)</strong>：模型从中学习规律
          <br>• <strong>验证集 (Validation)</strong>：用于调参与早停
          <br>• <strong>测试集 (Test)</strong>：只用于最终评估
        </div>

        <div class="controls">
          <div class="control-group">
            <label>数据总数</label>
            <input type="number" id="totalData" value="100" min="50" max="500">
          </div>
          <div class="control-group">
            <label>训练集比例 (%)</label>
            <input type="range" id="trainRatio" min="50" max="80" value="70">
            <span id="trainValue">70%</span>
          </div>
          <div class="control-group">
            <label>验证集比例 (%)</label>
            <input type="range" id="validRatio" min="10" max="30" value="15">
            <span id="validValue">15%</span>
          </div>
          <div class="control-group">
            <label>测试集比例 (%)</label>
            <span id="testValue">15%</span>
          </div>
        </div>

        <div class="legend">
          <div class="legend-item">
            <div class="legend-color train"></div><span>训练集</span>
          </div>
          <div class="legend-item">
            <div class="legend-color validation"></div><span>验证集</span>
          </div>
          <div class="legend-item">
            <div class="legend-color test"></div><span>测试集</span>
          </div>
        </div>

        <div class="dataset" id="dataVisualization"></div>

        <div class="stats">
          <div class="stat-card">
            <div class="stat-number" id="trainCount">70</div>
            <div class="stat-label">训练样本数</div>
          </div>
          <div class="stat-card">
            <div class="stat-number" id="validCount">15</div>
            <div class="stat-label">验证样本数</div>
          </div>
          <div class="stat-card">
            <div class="stat-number" id="testCount">15</div>
            <div class="stat-label">测试样本数</div>
          </div>
        </div>
      </div>

      <!-- 训练参数部分 -->
      <div class="section tab-panel" data-tab="part2">
        <h2>🎯 第二部分：训练参数概念</h2>
        <div class="info-box">
          <strong>关系：</strong> 一个 epoch = ⌈训练样本总数 ÷ batch size⌉ 次 iteration
        </div>

        <div class="controls">
          <div class="control-group">
            <label>Batch Size</label>
            <input type="number" id="batchSize" value="10" min="1" max="128">
          </div>
          <div class="control-group">
            <label>目标 Epochs</label>
            <input type="number" id="epochs" value="3" min="1" max="20">
          </div>
          <button class="btn" onclick="startTraining()">开始训练演示</button>
          <button class="btn" onclick="resetTraining()">重置</button>
        </div>

        <div class="training-demo">
          <div class="epoch-counter">当前进度：第 <span id="currentEpoch">0</span> 个 Epoch，第 <span
              id="currentIteration">0</span> 次 Iteration</div>
          <div class="progress-bar">
            <div class="progress-fill" id="progressFill"></div>
          </div>

          <div class="stats">
            <div class="stat-card">
              <div class="stat-number" id="totalIterations">0</div>
              <div class="stat-label">每个 Epoch 的 Iterations</div>
            </div>
            <div class="stat-card">
              <div class="stat-number" id="totalTrainingSteps">0</div>
              <div class="stat-label">总训练步数</div>
            </div>
          </div>

          <h3 style="margin-top:12px">Batch 划分可视化：</h3>
          <div class="batch-visualization" id="batchVisualization"></div>
        </div>
      </div>

      <!-- 知识测试部分 -->
      <div class="section tab-panel" data-tab="part3">
        <h2>🧪 第三部分：知识测试（5 题）</h2>
        <div class="quiz-container">
          <div class="quiz-question" id="quizQuestion"></div>
          <div class="quiz-options" id="quizOptions"></div>
          <div class="controls" style="gap:10px">
            <button class="btn" id="quizNext" style="display:none">下一题</button>
            <span class="hint" id="quizHint"></span>
          </div>
          <div class="stats">
            <div class="stat-card">
              <div class="stat-number" id="quizCorrect">0</div>
              <div class="stat-label">正确</div>
            </div>
            <div class="stat-card">
              <div class="stat-number" id="quizTotal">0</div>
              <div class="stat-label">已答</div>
            </div>
            <div class="stat-card">
              <div class="stat-number" id="quizAcc">0%</div>
              <div class="stat-label">正确率</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    let isTraining = false, currentEpoch = 0, currentIteration = 0, trainingInterval;

    // --------- 工具函数（用于测试与计算） ---------
    function computeIterationsPerEpoch(trainCount, batchSize) {
      return Math.ceil(trainCount / Math.max(1, batchSize));
    }
    function computeBatches(trainCount, batchSize) {
      return Math.ceil(trainCount / Math.max(1, batchSize));
    }
    function computeSplit(total, trainRatio, validRatio) {
      const train = Math.floor(total * trainRatio / 100);
      const valid = Math.floor(total * validRatio / 100);
      const test = total - train - valid;
      return { train, valid, test };
    }

    document.addEventListener('DOMContentLoaded', () => {
      // 初始化 UI
      updateDataVisualization();
      updateTrainingParams();

      // 事件监听
      document.getElementById('totalData').addEventListener('input', updateDataVisualization);
      document.getElementById('trainRatio').addEventListener('input', updateDataVisualization);
      document.getElementById('validRatio').addEventListener('input', updateDataVisualization);
      document.getElementById('batchSize').addEventListener('input', updateTrainingParams);
      document.getElementById('epochs').addEventListener('input', updateTrainingParams);

      // 选项卡绑定
      document.querySelectorAll('.tab').forEach(btn => {
        btn.addEventListener('click', () => setActive(btn.dataset.tabTarget));
      });

      // Quiz init
      loadQuiz();
      document.getElementById('quizNext').addEventListener('click', nextQuiz);

      // 轻量“测试用例”（输出到控制台，不影响页面）
      runTests();
    });

    function updateDataVisualization() {
      const totalData = parseInt(document.getElementById('totalData').value);
      const trainRatio = parseInt(document.getElementById('trainRatio').value);
      const validRatio = parseInt(document.getElementById('validRatio').value);
      const testRatio = 100 - trainRatio - validRatio;

      document.getElementById('trainValue').textContent = trainRatio + '%';
      document.getElementById('validValue').textContent = validRatio + '%';
      document.getElementById('testValue').textContent = testRatio + '%';

      const trainCount = Math.floor(totalData * trainRatio / 100);
      const validCount = Math.floor(totalData * validRatio / 100);
      const testCount = totalData - trainCount - validCount;

      document.getElementById('trainCount').textContent = trainCount;
      document.getElementById('validCount').textContent = validCount;
      document.getElementById('testCount').textContent = testCount;

      const visualization = document.getElementById('dataVisualization');
      visualization.innerHTML = '';
      for (let i = 0; i < totalData; i++) {
        const point = document.createElement('div');
        point.className = 'data-point';
        point.textContent = i + 1;
        if (i < trainCount) { point.classList.add('train'); point.title = `训练样本 ${i + 1}` }
        else if (i < trainCount + validCount) { point.classList.add('validation'); point.title = `验证样本 ${i + 1}` }
        else { point.classList.add('test'); point.title = `测试样本 ${i + 1}` }
        visualization.appendChild(point);
      }
      updateTrainingParams();
    }

    function updateTrainingParams() {
      const trainCount = parseInt(document.getElementById('trainCount').textContent);
      const batchSize = parseInt(document.getElementById('batchSize').value);
      const epochs = parseInt(document.getElementById('epochs').value);
      const iterationsPerEpoch = computeIterationsPerEpoch(trainCount, batchSize);
      const totalSteps = iterationsPerEpoch * epochs;
      document.getElementById('totalIterations').textContent = iterationsPerEpoch;
      document.getElementById('totalTrainingSteps').textContent = totalSteps;
      updateBatchVisualization();
    }

    function updateBatchVisualization() {
      const trainCount = parseInt(document.getElementById('trainCount').textContent);
      const batchSize = parseInt(document.getElementById('batchSize').value);
      const batches = computeBatches(trainCount, batchSize);
      const batchViz = document.getElementById('batchVisualization');
      batchViz.innerHTML = '';
      for (let i = 0; i < batches; i++) {
        const batch = document.createElement('div');
        batch.className = 'batch'; batch.id = `batch-${i}`;
        const title = document.createElement('div'); title.className = 'batch-title'; title.textContent = `Batch ${i + 1}`;
        const miniData = document.createElement('div'); miniData.className = 'mini-data';
        const actual = Math.min(batchSize, trainCount - i * batchSize);
        for (let j = 0; j < actual; j++) { const mp = document.createElement('div'); mp.className = 'mini-point'; miniData.appendChild(mp); }
        const sizeLabel = document.createElement('div'); sizeLabel.textContent = `${actual} 样本`; sizeLabel.style.fontSize = '12px'; sizeLabel.style.color = '#b7c6ff'; sizeLabel.style.marginTop = '6px';
        batch.appendChild(title); batch.appendChild(miniData); batch.appendChild(sizeLabel); batchViz.appendChild(batch);
      }
    }

    function startTraining() {
      if (isTraining) return; isTraining = true; currentEpoch = 0; currentIteration = 0;
      const iterationsPerEpoch = parseInt(document.getElementById('totalIterations').textContent);
      const totalEpochs = parseInt(document.getElementById('epochs').value);
      const totalSteps = iterationsPerEpoch * totalEpochs; let step = 0;
      trainingInterval = setInterval(() => {
        step++; currentIteration++;
        const batchIndex = (currentIteration - 1) % iterationsPerEpoch; clearBatchHighlight(); highlightBatch(batchIndex);
        if (currentIteration > iterationsPerEpoch) { currentEpoch++; currentIteration = 1; }
        document.getElementById('currentEpoch').textContent = currentEpoch + 1;
        document.getElementById('currentIteration').textContent = currentIteration;
        document.getElementById('progressFill').style.width = (step / totalSteps * 100) + '%';
        if (step >= totalSteps) { clearInterval(trainingInterval); isTraining = false; clearBatchHighlight(); alert('训练完成！🎉'); }
      }, 1000);
    }

    function resetTraining() {
      if (trainingInterval) clearInterval(trainingInterval); isTraining = false; currentEpoch = 0; currentIteration = 0;
      document.getElementById('currentEpoch').textContent = '0';
      document.getElementById('currentIteration').textContent = '0';
      document.getElementById('progressFill').style.width = '0%';
      clearBatchHighlight();
    }

    function highlightBatch(i) { const el = document.getElementById(`batch-${i}`); if (el) el.classList.add('active'); }
    function clearBatchHighlight() { document.querySelectorAll('.batch').forEach(b => b.classList.remove('active')) }

    /************* 简单知识测试（5题） *************/
    const QUIZ = [
      { q: '共有 800 个训练样本，batch size=64，则完成 1 个 epoch 需要多少次 iteration？', opts: ['12 次', '13 次', '64 次'], correct: 1 },
      { q: '用于选择超参数与早停的集合是：', opts: ['训练集', '验证集', '测试集'], correct: 1 },
      { q: '测试集的主要作用是：', opts: ['调整模型超参数', '评估最终泛化性能', '增大训练数据量'], correct: 1 },
      { q: '当 batch size 增大（其他不变）时，每个 epoch 的 iteration 数量将：', opts: ['增加', '减少', '不变'], correct: 1 },
      { q: '设比例为 70%/20%/10%，总样本 150，则三者样本数依次为：', opts: ['105/30/15', '100/30/20', '110/20/20'], correct: 0 },
    ];
    let qIdx = 0, qCorrect = 0, qTotal = 0;

    function loadQuiz() {
      const q = QUIZ[qIdx];
      document.getElementById('quizQuestion').textContent = q.q;
      const box = document.getElementById('quizOptions'); box.innerHTML = '';
      q.opts.forEach((opt, i) => {
        const el = document.createElement('div'); el.className = 'quiz-option'; el.textContent = opt;
        el.onclick = () => selectQuiz(i); box.appendChild(el);
      });
      document.getElementById('quizHint').textContent = '';
      document.getElementById('quizNext').style.display = 'none';
    }

    function selectQuiz(i) {
      const q = QUIZ[qIdx];
      const opts = document.querySelectorAll('#quizOptions .quiz-option');
      // 防止重复点击重复计数
      let alreadyAnswered = false;
      opts.forEach(el => { if (el.classList.contains('correct') || el.classList.contains('wrong')) alreadyAnswered = true; });
      if (alreadyAnswered) return;
      qTotal++;
      opts.forEach((el, idx) => { el.onclick = null; if (idx === q.correct) el.classList.add('correct'); else if (idx === i) el.classList.add('wrong'); });
      if (i === q.correct) { qCorrect++; document.getElementById('quizHint').textContent = '✅ 回答正确！'; }
      else { document.getElementById('quizHint').textContent = '❌ 回答错误！'; }
      updateQuizStats();
      const nextBtn = document.getElementById('quizNext');
      nextBtn.textContent = (qIdx === QUIZ.length - 1) ? '完成' : '下一题';
      nextBtn.style.display = 'inline-block';
    }

    function nextQuiz() {
      if (qIdx < QUIZ.length - 1) {
        qIdx++;
        loadQuiz();
      } else {
        const qEl = document.getElementById('quizQuestion');
        const optEl = document.getElementById('quizOptions');
        const hint = document.getElementById('quizHint');
        qEl.textContent = '🎉 本轮测试已完成';
        optEl.innerHTML = '';
        hint.textContent = '做得好！你可以切换到其他选项卡继续学习。';
        const btn = document.getElementById('quizNext');
        btn.style.display = 'none';
      }
    }

    function updateQuizStats() {
      document.getElementById('quizCorrect').textContent = qCorrect;
      document.getElementById('quizTotal').textContent = qTotal;
      const acc = qTotal ? Math.round(qCorrect / qTotal * 100) : 0;
      document.getElementById('quizAcc').textContent = acc + '%';
    }

    // 选项卡逻辑
    function setActive(tabKey) {
      document.querySelectorAll('.tab').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.tabTarget === tabKey);
      });
      document.querySelectorAll('.tab-panel').forEach(panel => {
        panel.classList.toggle('active', panel.dataset.tab === tabKey);
      });
    }

    // 轻量测试用例（不修改页面状态，仅向控制台输出）
    function runTests() {
      try {
        console.assert(computeIterationsPerEpoch(800, 64) === 13, 'Test#1: 800/64 应为 13 次');
        const s = computeSplit(150, 70, 20); // 70/20/10
        console.assert(s.train === 105 && s.valid === 30 && s.test === 15, 'Test#2: 150×(70/20/10) 应为 105/30/15');
        console.assert(computeBatches(70, 10) === 7, 'Test#3: 70 样本 batch=10 应为 7 批');
        const ite = computeIterationsPerEpoch(70, 10) * 3;
        console.assert(ite === 21, 'Test#4: 每 epoch 7 次 × 3 = 21 总步');
        console.log('%c所有内置测试通过', 'color:#46d082');
      } catch (e) {
        console.warn('测试出现异常', e);
      }
    }
  </script>
</body>

</html>